/*******************************************************************************
* Signavio Core Components
* Copyright (C) 2012 Signavio GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package org.oryxeditor.server.diagram.generic;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.oryxeditor.server.diagram.Bounds;
import org.oryxeditor.server.diagram.Point;
import org.oryxeditor.server.diagram.label.LabelSettings;
import org.oryxeditor.server.diagram.util.NumberUtil;
/**
* Implementation for the {@link GenericShape} interface
*
* @author Philipp Maschke, Robert Gurol
*
* @param <S> the actual type of shape to be used (must inherit from {@link GenericShape}); calls to {@link #getChildShapesReadOnly()}, ... will return this type
* @param <D> the actual type of diagram to be used (must inherit from {@link GenericDiagram}); {@link #getDiagram()} will return this type
*/
public abstract class GenericShapeImpl<S extends GenericShape<S,D>, D extends GenericDiagram<S,D>> implements GenericShape<S, D>{
/**
* a shape is an edge if it has more than 1 docker; a shape with only 1 or 0 dockers will always be a node
* @param dockers
* @return whether the shape having the given dockers is an edge (if false, it is a node)
*/
protected static boolean isEdge(List<Point> dockers){
return dockers.size() > 1;
}
// Refers to a stencil; in contrast to the stencil, contains a stencilset
// reference as well.
// protected StencilReference stencilRef;
protected String stencilId;
// shape's (resource) ID
protected String resourceId;
// properties (such as "name")
protected Map<String, String> properties;
protected Map<String, Class<?>> propertyTypes;
// all direct children of the shape (e.g., tasks etc. in a BPMN pool)
protected List<S> childShapes;
// direct parent of the shape (e.g., a pool as parent of a BPMN tasks etc.)
protected S parent;
//TODO change to Set<X>?
// All outgoing shapes. Outgoing shapes are somehow connected to this shape
// (like BPMN boundary events, or outgoing sequence flows of a task)
protected List<S> outgoings;
// All incoming shapes. Incoming shapes are somehow connected to this shape
// (like a BPMN sequence flow that points to a task (this)).
protected List<S> incomings;
// Docker points within the shape. BPMN's events have one central docker,
// other shapes with (more) dockers are edges. For edges, dockers are points
// between which there are straight lines (e.g., a straight sequence flow
// can have only two dockers; one that makes a 90 degree turn has at least
// three).
protected List<Point> dockers;
//
protected Map<String, LabelSettings> labelSettings;
// bounds represent the boundary of a shape, its maximum extension; they are
// defined by an upper left and a lower right Point
protected Bounds bounds;
// the diagram containing the shape; is calculated
protected D diagram;
/**
* Constructs a new shape with id and stencilRef
*
* @param resourceId
* unique shape id, generated by the editor
* @param stencilId
* StencilType with stencilId
*/
public GenericShapeImpl(String resourceId, String stencilId) {
this(resourceId);
this.stencilId = stencilId;
}
/**
* Set a new id for the shape
*
* @param resourceId
*/
public GenericShapeImpl(String resourceId) {
this.resourceId = resourceId;
this.properties = new HashMap<String, String>();
this.propertyTypes = new HashMap<String, Class<?>>();
this.childShapes = new LinkedList<S>();
this.outgoings = new LinkedList<S>();
this.incomings = new LinkedList<S>();
this.dockers = new ArrayList<Point>(5);//needs random access, usually no more than 4 dockers
this.labelSettings = new HashMap<String, LabelSettings>();
this.bounds = new Bounds();
this.stencilId = null;
}
public boolean isEdge(){
return this instanceof GenericEdge<?, ?>;
}
public boolean isNode(){
return this instanceof GenericNode<?, ?>;
}
public String getStencilId() {
return stencilId;
}
public void setStencilId(String stencilId) {
this.stencilId = stencilId;
}
public String getResourceId() {
return resourceId;
}
public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}
public Map<String, String> getPropertiesReadOnly() {
return Collections.unmodifiableMap(this.properties);
}
public void setProperties(Map<String, String> properties) {
this.properties.clear();
propertyTypes.clear();
if (properties != null){
for (Map.Entry<String, String> entry: properties.entrySet()){
setProperty(entry.getKey(), entry.getValue());
}
}
}
public boolean hasProperty(String name) {
return this.properties.containsKey(name);
}
public Set<String> getPropertyNames() {
return this.properties.keySet();
}
public String getProperty(String name) {
return this.properties.get(name);
}
public Object getPropertyObject(String name) {
Class<?> type = propertyTypes.get(name);
if (type == null || type.equals(String.class))
return getProperty(name);
else if (type.equals(Integer.class))
return getPropertyInteger(name);
else if (type.equals(Long.class))
return getPropertyLong(name);
else if (type.equals(Float.class))
return getPropertyFloat(name);
else if (type.equals(Double.class))
return getPropertyDouble(name);
else if (type.equals(Boolean.class))
return getPropertyBoolean(name);
else if (type.equals(JSONObject.class))
return getPropertyJsonObject(name);
else if (type.equals(JSONArray.class))
return getPropertyJsonArray(name);
else
throw new RuntimeException("Property supposedly of type '" +
type.getName() +
"' but only the following are supported: String, Integer, Long, Double, Float, Boolean, JSONObject, JSONArray");
}
public Integer getPropertyInteger(String name) {
return NumberUtil.createInt(getProperty(name));
}
public Long getPropertyLong(String name) {
return NumberUtil.createLong(getProperty(name));
}
public Float getPropertyFloat(String name) {
return NumberUtil.createFloat(getProperty(name));
}
public Double getPropertyDouble(String name) {
return NumberUtil.createDouble(getProperty(name));
}
public Boolean getPropertyBoolean(String name) {
String value = getProperty(name);
if ("true".equalsIgnoreCase(value))
return true;
else if ("false".equalsIgnoreCase(value))
return false;
else
return null;
}
public JSONObject getPropertyJsonObject(String name){
String value = getProperty(name);
if (value != null && value.startsWith("{") && value.endsWith("}")){
try{
return new JSONObject(value);
}catch(JSONException e){
return null;
}
}else
return null;
}
public JSONArray getPropertyJsonArray(String name){
String value = getProperty(name);
if (value != null && value.startsWith("[") && value.endsWith("]")){
try{
return new JSONArray(value);
}catch(JSONException e){
return null;
}
}else
return null;
}
public String removeProperty(String name){
propertyTypes.remove(name);
return this.properties.remove(name);
}
public String setProperty(String name, String value) {
propertyTypes.put(name, String.class);
return this.properties.put(name, (value == null)? null : value.trim());
}
public String setProperty(String name, Object value) {
if (value == null || value instanceof String)
return setProperty(name, (String)value);
else if (value instanceof Integer)
return setProperty(name, ((Integer)value).intValue());
else if (value instanceof Long)
return setProperty(name, ((Long)value).longValue());
else if (value instanceof Double)
return setProperty(name, ((Double)value).doubleValue());
else if (value instanceof Float)
return setProperty(name, ((Float)value).floatValue());
else if (value instanceof Boolean)
return setProperty(name, ((Boolean)value).booleanValue());
else if (value instanceof JSONObject)
return setProperty(name, (JSONObject)value);
else if (value instanceof JSONArray)
return setProperty(name, (JSONArray)value);
else
throw new IllegalArgumentException("Value is of type '" +
value.getClass().getName() +
"' but must be one of the following: String, Integer, Long, Double, Float, Boolean, JSONObject, JSONArray");
}
public String setProperty(String name, int value) {
propertyTypes.put(name, Integer.class);
return properties.put(name, Integer.toString(value));
}
public String setProperty(String name, long value) {
propertyTypes.put(name, Long.class);
return properties.put(name, Long.toString(value));
}
public String setProperty(String name, float value) {
propertyTypes.put(name, Float.class);
return properties.put(name, Float.toString(value));
}
public String setProperty(String name, double value) {
propertyTypes.put(name, Double.class);
return properties.put(name, Double.toString(value));
}
public String setProperty(String name, boolean value) {
propertyTypes.put(name, Boolean.class);
return properties.put(name, Boolean.toString(value));
}
public String setProperty(String name, JSONObject value){
propertyTypes.put(name, JSONObject.class);
return properties.put(name, (value == null)? null : value.toString());
}
public String setProperty(String name, JSONArray value){
propertyTypes.put(name, JSONArray.class);
return properties.put(name, (value == null)? null : value.toString());
}
public List<S> getChildShapesReadOnly() {
return Collections.unmodifiableList(this.childShapes);
}
public Set<S> getDescendantShapesReadOnly() {
Set<S> childShapes = new HashSet<S>();
for (S childShape : this.getChildShapesReadOnly()) {
childShapes.addAll(childShape.getDescendantShapesReadOnly());
childShapes.add(childShape);
}
return Collections.unmodifiableSet(childShapes);
}
public List<S> getAncestorShapesReadOnly() {
List<S> resList = new LinkedList<S>();
S parent = this.getParent();
while(parent != null){
resList.add(parent);
parent = (S) parent.getParent();
}
return Collections.unmodifiableList(resList);
}
public void setChildShapes(List<S> childShapes) {
this.childShapes.clear();
if (childShapes == null)
return;
for (S shape : childShapes) {
addChildShape(shape);
}
}
public void addChildShape(S shape) {
if (shape == null || shape == this)
return;
if (shape instanceof GenericDiagram)
return;
//TODO add circular dependency check! (check whether new shape is already an ancestor of this shape)
if (!this.childShapes.contains(shape))
this.childShapes.add(shape);
// distinction unnecessary, as setParent is now side effect free again
// if (shape.getParent() != this)
shape.setParent((S) this);
addToDiagramShapeCache(shape);
}
public int getNumChildShapes(){
return childShapes.size();
}
public void setDiagram(D diagram2) {
this.diagram = diagram2;
}
public void removeChildShape(S shape) {
if (shape == null)
return;
childShapes.remove(shape);
removeFromDiagramShapeCache(shape);
// has to be last, as the getDiagram method relies on a parent being
// present
shape.setParent(null);
}
public void removeAllChildShapes(){
D diagram = getDiagram();
for (S child: childShapes){
child.removeAllChildShapes();
if (diagram != null)
diagram.removeFromAllShapes(child);
child.setDiagram(null);
// has to be last, as the getDiagram method relies on a parent being
// present
child.setParent(null);
}
childShapes.clear();
}
public S getParent() {
return parent;
}
public void setParent(S parent) {
this.parent = parent;
}
public void setParentAndUpdateItsChildShapes(S parent) {
if (this.parent != null) {
this.parent.getChildShapesReadOnly().remove(this);
}
this.parent = parent;
if (parent != null && !parent.hasChild((S) this))
parent.addChildShape((S) this);
}
public D getDiagram() {
if (this.diagram == null) {
// Lookup the parent till diagram is found
S parent = this.getParent();
while (parent != null && !(parent instanceof GenericDiagram))
parent = (S) parent.getParent();
if (parent instanceof GenericDiagram)
this.diagram = (D) parent;
}
return this.diagram;
}
public void addDocker(Point p) {
this.dockers.add(p);
}
public void addDocker(Point p, int position) {
this.dockers.add(position, p);
}
/**
* Returns an unmodifiable view on the shape's dockers.
*/
public List<Point> getDockersReadOnly() {
return Collections.unmodifiableList(dockers);
}
public int getNumDockers(){
return this.dockers.size();
}
public void setDockers(List<Point> dockers) {
this.dockers.clear();
if (dockers != null)
this.dockers.addAll(dockers);
}
public Point getDockerAt(int index){
if (index < 0 || index >= this.dockers.size())
return null;
else
return this.dockers.get(index);
}
public void removeDockerAt(int index) {
this.dockers.remove(index);
}
public Bounds getBounds() {
return bounds.copy();
}
public Bounds getAbsoluteBounds() {
Bounds bounds = this.bounds.copy();
S parent = this.getParent();
while (parent != null) {
bounds.moveBy(parent.getUpperLeft());
parent = (S) parent.getParent();
}
return bounds;
}
public void setBounds(Bounds bounds) {
if (bounds == null)
this.bounds = null;
else
this.bounds = bounds.copy();
}
/**
* Returns an unmodifiable view on the shape's incoming shapes
*/
public List<S> getIncomingsReadOnly() {
return Collections.unmodifiableList(this.incomings);
}
protected void setIncomings(List<S> incomings) {
this.incomings.clear();
if (incomings != null)
this.incomings.addAll(incomings);
}
public boolean hasIncoming(S shape) {
return this.incomings.contains(shape);
}
public int getNumIncomings(){
return incomings.size();
}
public void setIncomingsAndUpdateTheirOutgoings(List<S> incomings) {
for (S shape : this.incomings) {
// if (shape.hasOutgoing((X) this))
getImplClass(shape).removeOutgoing((S) this);
}
this.setIncomings(incomings);
for (S shape : this.incomings) {
if (!shape.hasOutgoing((S) this))
getImplClass(shape).addOutgoing((S) this);
}
}
protected boolean addIncoming(S shape) {
if (shape != null && !this.incomings.contains(shape))
return this.incomings.add(shape);
return false;
}
public boolean addIncomingAndUpdateItsOutgoings(S shape) {
if (shape != null && !shape.hasOutgoing((S) this))
getImplClass(shape).addOutgoing((S) this);
return this.addIncoming(shape);
}
protected boolean removeIncoming(S shape) {
return this.incomings.remove(shape);
}
public boolean removeIncomingAndUpdateItsOutgoings(S shape) {
if (shape != null)
getImplClass(shape).removeOutgoing((S) this);
return removeIncoming(shape);
}
public List<S> getOutgoingsReadOnly() {
return Collections.unmodifiableList(outgoings);
}
protected void setOutgoings(List<S> outgoings) {
this.outgoings.clear();
if (outgoings != null)
this.outgoings.addAll(outgoings);
}
public boolean hasOutgoing(S shape) {
return this.outgoings.contains(shape);
}
public int getNumOutgoings(){
return outgoings.size();
}
public void setOutgoingsAndUpdateTheirIncomings(List<S> outgoings) {
for (S shape : this.outgoings) {
// if (shape.hasIncoming((X) this))
getImplClass(shape).removeIncoming((S) this);
}
this.setOutgoings(outgoings);
for (S shape : this.outgoings) {
if (!shape.hasIncoming((S) this))
getImplClass(shape).addIncoming((S) this);
}
}
protected boolean addOutgoing(S shape) {
if (shape != null && !this.outgoings.contains(shape))
return this.outgoings.add(shape);
return false;
}
public boolean addOutgoingAndUpdateItsIncomings(S shape) {
if (shape != null && !shape.hasIncoming((S) this))
getImplClass(shape).addIncoming((S) this);
return this.addOutgoing(shape);
}
protected boolean removeOutgoing(S shape) {
return this.outgoings.remove(shape);
}
public boolean removeOutgoingAndUpdateItsIncomings(S shape) {
if (shape != null)
getImplClass(shape).removeIncoming((S) this);
return this.removeOutgoing(shape);
}
public List<S> getConnectedShapesReadOnly() {
List<S> shapes = new ArrayList<S>();
shapes.addAll(this.getIncomingsReadOnly());
shapes.addAll(this.getOutgoingsReadOnly());
return Collections.unmodifiableList(shapes);
}
public Point getUpperLeft() {
if (this.bounds != null)
return this.bounds.getUpperLeft();
return null;
}
public Point getLowerRight() {
if (this.bounds != null)
return this.bounds.getLowerRight();
return null;
}
public double getHeight() {
return this.getBounds().getHeight();
}
public double getWidth() {
return this.getBounds().getWidth();
}
public int hashCode() {
return resourceId.hashCode();
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
if (obj instanceof GenericShapeImpl<?,?>){
GenericShapeImpl<?,?> other = (GenericShapeImpl<?,?>) obj;
if (resourceId == null) {
if (other.getResourceId() != null)
return false;
} else if (!resourceId.equals(other.getResourceId()))
return false;
return true;
}
return false;
}
public LabelSettings getLabelSettingsForReference(String referencedLabel) {
return labelSettings.get(referencedLabel);
}
public Collection<LabelSettings> getLabelSettings() {
return labelSettings.values();
}
public void setLabelSettings(Collection<LabelSettings> labelPositionings) {
this.labelSettings.clear();
if (labelPositionings == null)
return;
for (LabelSettings alignment : labelPositionings) {
this.labelSettings.put(alignment.getReference(), alignment);
}
}
public void addLabelSetting(LabelSettings newSetting) {
if (newSetting != null)
this.labelSettings.put(newSetting.getReference(), newSetting);
}
public boolean isPointIncluded(Point p) {
return this.bounds.isPointIncluded(p);
}
public boolean isPointIncludedAbsolute(Point p) {
return this.getAbsoluteBounds().isPointIncluded(p);
}
public boolean hasChild(S s) {
if (this.getChildShapesReadOnly().contains(s))
return true;
return false;
}
public boolean contains(S s) {
if (this.hasChild(s))
return true;
for (S s2 : this.getChildShapesReadOnly()) {
if (s2.hasChild(s))
return true;
}
return false;
}
public String getQualifiedStencilId() {
if (getDiagram() != null && getDiagram().getStencilsetRef() != null && getDiagram().getStencilsetRef().getNamespace() != null)
return getDiagram().getStencilsetRef().getNamespace() + getStencilId();
else
return getStencilId();
}
/**
* Adds the shape and if needed all its child shapes (recursively) to the cache of all shapes in the diagram
* @param shape
*/
private void addToDiagramShapeCache(S shape) {
// method is used in Diagram as well, but no special treatment is
// necessary, as Diagram#getDiagram returns "this"
D diagram = this.getDiagram();
if (diagram != null && !diagram.containsShape(shape)) {
diagram.addToAllShapes(shape);
shape.setDiagram(diagram);
//update all child shapes of the new shape too!
for (S child: shape.getChildShapesReadOnly())
addToDiagramShapeCache(child);
}
}
/**
* Removes the shape and if needed all its child shapes (recursively) from the cache of all shapes in the diagram
* @param shape
*/
private void removeFromDiagramShapeCache(S shape) {
D diagram = (D) shape.getDiagram();
if (diagram != null && diagram.containsShape(shape)){
diagram.removeFromAllShapes(shape);
shape.setDiagram(null);
//update all child shapes of the new shape too!
for (S child: shape.getChildShapesReadOnly())
removeFromDiagramShapeCache(child);
}
}
protected GenericShapeImpl<S, D> getImplClass(S shape){
if (shape instanceof GenericShapeImpl)
return (GenericShapeImpl<S,D>)shape;
else
throw new RuntimeException("Discovered instance of " + GenericShape.class.getSimpleName() +
", whose implementing class is not " + GenericShapeImpl.class.getSimpleName());
}
}